using System;
using System.Globalization;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core
{	
	/// <summary>
	/// Error information for a field.
	/// </summary>
	public sealed class FieldError
	{
		#region Members.

		IDbColumn field;
		FieldErrorCode errorCode;
		object value;

		#endregion

		#region Constructors.

		/// <summary>
		/// Initializes a new FieldError structure.
		/// </summary>
		/// <remarks>Validates the given value and sets the <see cref="ErrorCode"/> property.</remarks>
		/// <param name="field">Field.</param>		
		/// <param name="value">Value that has been tested.</param>
		public FieldError(IDbColumn field, object value)
		{
			this.field = field;			
			this.value = value;
			FieldValidator validator = new FieldValidator(field);
			this.errorCode = validator.Validate(value);
		}

		/// <summary>
		/// Initializes a new FieldError structure.
		/// </summary>
		/// <remarks>Used when the value has already been validate and the error code has already been computed.</remarks>
		/// <param name="field">Field.</param>
		/// <param name="errorCode">Error.</param>
		/// <param name="value">Value that has been tested.</param>
		public FieldError(IDbColumn field, FieldErrorCode errorCode, object value)
		{
			this.field = field;
			this.errorCode = errorCode;
			this.value = value;
		}

		#endregion

		#region Properties.

		/// <summary>
		/// Gets the field for which this structure contains error information.
		/// </summary>
		public IDbColumn Field
		{
			get {return this.field;}
		}

		/// <summary>
		/// Gets the errorcode for this' structure field and value.
		/// </summary>
		public FieldErrorCode ErrorCode
		{
			get {return this.errorCode;}
		}

		/// <summary>
		/// Gets the value that has been tested.
		/// </summary>
		public object Value
		{
			get {return this.value;}
		}

		#endregion
	}

	/// <summary>
	/// Field error codes.
	/// </summary>
	public enum FieldErrorCode
	{
		#region Values.

		/// <summary>
		/// Value has passed validation.
		/// </summary>
		AllOk = 0,

		/// <summary>
		/// Unknown error occured.
		/// </summary>
		ErrorOther = 1,

		/// <summary>
		/// Value is null but the field is not nullable.
		/// </summary>
		NullError = 2,

		/// <summary>
		/// Value is out of allowed range.
		/// </summary>
		OutOfRangeError = 3,

		/// <summary>
		/// Value is not compatibile with the field's type.
		/// </summary>
		NonCompatibleType = 4

		#endregion
	}

	/// <summary>
	/// Performs field validation.
	/// </summary>
    /// <remarks>Checks whether the value contained in the given field obeys basic constraints 
    /// (nullability, min/max value etc). Returns <see cref="FieldErrorCode"/> that indicates 
    /// the type of error encountered.</remarks>
    /// <returns>Error code.</returns>
    /// <example>
    /// The folowing example tests values against the HireDate field of the Northwind's Employees table.
    /// HireDate is a nullable T-SQL DateTime type (SQL Server). Minimum DateTime value supported by SQL Server is January 1, 1753.
    /// <code>
    /// EmployeesMeta employees = new EmployeesMeta();
    /// 
    /// // The field is nullable.
    /// // Validate method returns FieldErrorCode.AllOk.
    /// FieldErrorCode allOk = employees.HireDate.Validate(null);
    /// 
    /// // Minimum DateTime value supported by SQL Server is January 1, 1753.
    /// // Validate method returns FieldErrorCode.OutOfRangeError.
    /// FieldErrorCode outOfRangeError = employees.HireDate.Validate(new DateTime(1000, 1, 1));
    /// 
    /// // The field cannot accept integer values.
    /// // Validate method returns FieldErrorCode.NonCompatibileType.
    /// FieldErrorCode nonCompatibileType = employees.HireDate.Validate(0);
    /// </code>
    /// </example>
	[Serializable]
	sealed class FieldValidator
	{
		#region Private members.

		TypeClass typeClass;

		/// <summary>
		/// Field type.
		/// </summary>
		Type type;

		/// <summary>
		/// Minimum value allowed. May be null.
		/// </summary>
		object minValue;

		/// <summary>
		/// Maximum value allowed. May be null.
		/// </summary>
		object maxValue;

		/// <summary>
		/// Indicates whether null values are allowed.
		/// </summary>
		bool allowNull;

		/// <summary>
		/// Indicates whether the field is an autonumber field.
		/// </summary>
		bool autoIncrement;

		#endregion

		#region Constructors.

		/// <summary>
		/// Creates a new validator for the given column.
		/// </summary>
		/// <param name="column">Database column.</param>
		public FieldValidator(IDbColumn column)
		{
			this.type = column.DataType;
			this.minValue = column.MinValue;
			this.maxValue = column.MaxValue;
			this.allowNull = column.IsNullable || column.IsAutoGenerated;
			this.autoIncrement = column.AutoIncrement;

			this.typeClass = GetTypeClass(this.type);
		}

		#endregion

		#region Validate methods.
	
		/// <summary>
		/// Validates the given value.
		/// </summary>
		/// <param name="value">Value that will be validated.</param>
		/// <returns>Error code.</returns>
        /// <remarks>Checks whether the value contained in the given field obeys basic constraints 
        /// (nullability, min/max value etc). Returns <see cref="FieldErrorCode"/> that indicates 
        /// the type of error encountered.</remarks>
        /// <returns>Error code.</returns>
        /// <example>
        /// The folowing example tests values against the HireDate field of the Northwind's Employees table.
        /// HireDate is a nullable T-SQL DateTime type (SQL Server). Minimum DateTime value supported by SQL Server is January 1, 1753.
        /// <code>
        /// EmployeesMeta employees = new EmployeesMeta();
        /// 
        /// // The field is nullable.
        /// // Validate method returns FieldErrorCode.AllOk.
        /// FieldErrorCode allOk = employees.HireDate.Validate(null);
        /// 
        /// // Minimum DateTime value supported by SQL Server is January 1, 1753.
        /// // Validate method returns FieldErrorCode.OutOfRangeError.
        /// FieldErrorCode outOfRangeError = employees.HireDate.Validate(new DateTime(1000, 1, 1));
        /// 
        /// // The field cannot accept integer values.
        /// // Validate method returns FieldErrorCode.NonCompatibileType.
        /// FieldErrorCode nonCompatibileType = employees.HireDate.Validate(0);
        /// </code>
        /// </example>
		public FieldErrorCode Validate(object value)
		{
			if (value == null)
			{
				if (this.allowNull || this.autoIncrement)
					return FieldErrorCode.AllOk;
				else
					return FieldErrorCode.NullError;
			}

			switch (this.typeClass)
			{
				case TypeClass.Integer:
					return ValidateInt(value);					
				case TypeClass.Text:
					return ValidateText(value);
				case TypeClass.Date:
					return ValidateDate(value);
				case TypeClass.RealNumber:
					return ValidateReal(value);
				default:
					// E.g. byte array.
					return FieldErrorCode.AllOk;
			}
		}		

		private FieldErrorCode ValidateInt(object value)
		{
			if (TypeUtil.IsInteger(value.GetType()) == false)
			{
				return FieldErrorCode.NonCompatibleType;
			}
			else
			{
				long intVal = Convert.ToInt64(value, CultureInfo.InvariantCulture);
				if (this.minValue != null  &&  intVal < Convert.ToInt64(this.minValue, CultureInfo.InvariantCulture))
					return FieldErrorCode.OutOfRangeError;

                if (this.maxValue != null && intVal > Convert.ToInt64(this.maxValue, CultureInfo.InvariantCulture))
					return FieldErrorCode.OutOfRangeError;				
			}
			
			return FieldErrorCode.AllOk;		 
		}

		private FieldErrorCode ValidateReal(object value)
		{
			if (TypeUtil.IsNumber(value.GetType()) == false)
			{
				return FieldErrorCode.NonCompatibleType;
			}
			else
			{
                double intVal = Convert.ToDouble(value, CultureInfo.InvariantCulture);
                if (this.minValue != null && intVal < Convert.ToDouble(this.minValue, CultureInfo.InvariantCulture))
					return FieldErrorCode.OutOfRangeError;

                if (this.maxValue != null && intVal > Convert.ToDouble(this.maxValue, CultureInfo.InvariantCulture))
					return FieldErrorCode.OutOfRangeError;
			}
			
			return FieldErrorCode.AllOk;		 
		}

		private FieldErrorCode ValidateDate(object value)
		{
			if (TypeUtil.IsDate(value.GetType()) == false)
			{
				return FieldErrorCode.NonCompatibleType;
			}
			else
			{
				DateTime date = (DateTime) value;
				if (this.minValue != null  &&  date < (DateTime) this.minValue)
					return FieldErrorCode.OutOfRangeError;
				
				if (this.maxValue != null  &&  date > (DateTime) this.maxValue)
					return FieldErrorCode.OutOfRangeError;
			}

			return FieldErrorCode.AllOk;
		}

		private static FieldErrorCode ValidateText(object value)
		{
			if (TypeUtil.IsText(value.GetType()) == false)
				return FieldErrorCode.NonCompatibleType;
			else
				return FieldErrorCode.AllOk;
		}

		#endregion

		#region TypeClass enumeration, create and Is methods.

		private enum TypeClass
		{
			Integer,
			RealNumber,
			Text,
			Date,
			Other
		}		

		private static TypeClass GetTypeClass(Type dotNetType)
		{
			if (TypeUtil.IsText(dotNetType))
				return TypeClass.Text;
			else if (TypeUtil.IsDate(dotNetType))
				return TypeClass.Date;
			else if (TypeUtil.IsInteger(dotNetType))
				return TypeClass.Integer;
			else if (TypeUtil.IsFloatingPointNumber(dotNetType))
				return TypeClass.RealNumber;
			else
				return TypeClass.Other;
		}

		#endregion
	}
}
